/**
 * The contents of this file are subject to the license and copyright detailed in the LICENSE and NOTICE files at the root of the source tree and available online at
 * http://www.askosi.org/license.html
 */
package be.destin.util;

import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;

import org.apache.log4j.Logger;

import be.destin.skos.search.CountingThread;

/**
 * Allows to run synchronously external programs or operating system commands scripts, providing an input (as a string) and receiving output and errors (also as strings).
 * 
 * Has been proven rather robust: execution of GraphWiz, batch files with FTP transfer...
 * 
 * @author Christophe Dupriez
 *
 */
public class RunCommand implements Runnable {

	/** log4j logger */
	private static Logger log = Logger.getLogger(CountingThread.class);
	private static long seq = 0;

	String[] toDo;
	File dirFile;
	String[] environment;
	int exitValue = -1;
	
	char[] input;
	StringBuffer errors = null;
	StringBuffer output = null;

	/**
	 * Prepares a command line to be executed.
	 * 
	 * @param directory	Default directory when executing
	 * @param commandSegments	Individual segments of the command line to be executed.
	 * @param envp	Environment variables (Each string in the format KEY=VALUE)
	 */
	public RunCommand(String directory, String[] commandSegments, String[] envp) {
		seq++;
		toDo = commandSegments;
		environment = envp;
		dirFile = new File(directory);
	}

	public static void closeAble (Closeable toClose, String mess) {
		if (toClose != null) {
			try {
				toClose.close();
			} catch (IOException e) {
				log.error(e.toString()+" while closing "+mess);
			}
		}
	}

	/**
	 * Execute the command line and wait for its completion.
	 * 
	 * @see java.lang.Runnable#run()
	 */
	public void run () {
		if (toDo == null || toDo.length==0) return;
		for (int i = 0; i < toDo.length; i++) {
			toDo[i] = Util.replaceAllNoRegExp(toDo[i], "[seq]", ""+seq);
		}
		OutputStream inputStream = null;
		OutputStreamWriter in = null;
		InputStream outputStream = null;
		InputStreamReader out = null;
		InputStream errorsStream = null;
		InputStreamReader err = null;
		String toDoGlue = Util.glue(toDo," ");
		try {

			Process proc = Runtime.getRuntime().exec(toDo, environment, dirFile);
			exitValue = -1;

			if (input != null) {
				inputStream = proc.getOutputStream();
				in = new OutputStreamWriter(inputStream);
				in.write(input, 0, input.length);
				in.flush();
				in.close();
			}

			outputStream = proc.getInputStream();
			out = new InputStreamReader(outputStream);
			errorsStream = proc.getErrorStream();
			err = new InputStreamReader(errorsStream);

			boolean outOpen = true;
			output = new StringBuffer();
			boolean errOpen = true;
			errors = new StringBuffer();
			while (outOpen || errOpen) {
				while (out.ready()) {
					output.append((char)out.read());
				}
				while (err.ready()) {
					errors.append((char)err.read());
				}
				Thread.yield();
				outOpen = out.ready();
				errOpen = err.ready();
				if (!outOpen && !errOpen) {
					try {
						exitValue = proc.exitValue();
						outOpen = out.ready();
						errOpen = err.ready();
					} catch (IllegalThreadStateException  itse) {
						exitValue = -1;
						Thread.sleep(200);
						outOpen = true;
						errOpen = true;
					}
				}
			}
			if (exitValue < 0) {
				exitValue = proc.waitFor();
			}
			if (exitValue != 0) {
				log.error("ERROR LEVEL "+exitValue+":"+toDoGlue);
			}
		}
		catch (IOException ioe) {
			log.error(Util.glue(toDo," "),ioe);
		}
		catch (InterruptedException inte) {
			log.error(Util.glue(toDo," "),inte);
		}
		finally {
			closeAble (in,"Input writer for "+toDoGlue);
			closeAble (inputStream,"Input write stream for "+toDoGlue);
			closeAble (err,"Error reader for "+toDoGlue);
			closeAble (errorsStream,"Error read stream for "+toDoGlue);
			closeAble (out,"Output reader for "+toDoGlue);
			closeAble (outputStream,"Output read stream for "+toDoGlue);
		}
	}
	
	/**
	 * sequence: rank of the last execution.
	 * 
	 * @return	How many times a command was executed 
	 */
	public long getSequence() {
		return seq;
	}

	/**
	 * System.err() from executed command.
	 * 
	 * @return	A String with what the executed command wrote on stderr: 
	 */
	public String getErrors() {
		if (errors == null) return null;
		return errors.toString();
	}

	/**
	 * System.out() from executed command.
	 * 
	 * @return	A String with what the executed command wrote on stdout:
	 */
	public String getOutput() {
		if (output == null) return null;
		return output.toString();
	}

	/**
	 * @return	the String which has been provided as stdin: for command
	 */
	public String getInput() {
		if (input == null) return null;
		return new String(input);
	}

	/**
	 * Input for executed command.
	 * 
	 * @param input	String provided as the stdin: for the command to be executed
	 */
	public void setInput(String input) {
		if (input==null) this.input = null;
		else {
			this.input = input.toCharArray();
		}
	}

	/**
	 * Exit code.
	 * 
	 * @return	executed command exit code (0 = success)
	 */
	public int getExitValue() {
		return exitValue;
	}

	/**
	 * No foreseen use.
	 * @return	Set the exit code
	 */
	public void setExitValue(int exitValue) {
		this.exitValue = exitValue;
	}
}
